package au.edu.swin.MelbourneMap;

import java.io.*;
import java.util.*;
import java.awt.geom.*;

/**
 * Implementation of Model class using Model View Controller architecture.
 *
 * @author  Alexis Wellavize (s/n: 6033482)<br/>
 *           Dotan Tamir (s/n: 6532969)
 * @since   v1, created:  17/09/2012
 * @version v2, modified:  22/09/2012
 *
 */
 
public class Model
{
    // Hashmap containing regions read from file
    private HashMap<String, Region> regions;
    // ArrayList containing roads read from file    
    private ArrayList<Road> roads;
    // Easting and northing values
    private Double easting, northing;
    // Maximum and minimum easting and northing values
    private Double maxE, maxN, minE, minN;
    // Scale factors for X and Y axis
    private Double xScale, yScale;
    // Translation for X and Y axis
    private Double xTranslate, yTranslate;
    // Width and height of map
    private Integer height, width;
    // Scenario filename loaded
    private String scenarioFileName;
    // Label visibility flag
    private boolean labelVisibility;
    //set to true when senario file is read
    private boolean fileIsLoaded;
    private int transparencyFactor;
    // Flags to determine whether data has changed
    private boolean roadDataChanged;
    private boolean regionDataChanged;
    private boolean railDataChanged;
    // Flags to determine what is being done with data
    private boolean renameRoad = false;
    private boolean changeRegionColor = false;
    private boolean showShortestPath = false;
    private boolean editPoint = false;
	// Rectangle for zoom box
    private Path2D rectangle;
    // Road data structure
    RoadData roadData = new RoadData();
    
    
    // Default constructor
    public Model()
    {
        regions = new HashMap<String, Region>();
        roads = new ArrayList<Road>();
        RoadData roadData = new RoadData();
        scenarioFileName = "none";
        labelVisibility = false;
        xTranslate = 0.0;
        yTranslate = 0.0;
        xScale = 0.0;
        yScale = 0.0;
        width = 0;
        height = 0;
        maxE = 0.0;
        maxN = 0.0;
        minE = 0.0;
        minN = 0.0;
        fileIsLoaded = false;
        transparencyFactor = 0;
    }
    
    // Mutators
    
    /**
     * Sets the x and y translation of the map.
     */    
    public void setTranslation( double xTranslate, double yTranslate )
    {
        this.xTranslate += xTranslate;
        this.yTranslate += yTranslate;
    }

    /**
     * Sets the scale of the map
     */    
    public void setScale( double zoomFactor )
    {
        xScale = xScale * zoomFactor;
        yScale = yScale * zoomFactor;
    }

    /**
     * Sets the height of the map.
     */    
    public void setHeight( int height )
    {
        this.height = height;
    }

    /**
     * Sets the width of the map.
     */    
    public void setWidth( int width )
    {
        this.width = width;
    }

    /**
     * Resets the scale of the map back to orignal scale.
     */    
    public void resetScale()
    {
        xTranslate = Math.abs(minE);
        yTranslate = -(Math.abs(maxN));
        Double oldHeight = (maxN - minN);
        Double oldWidth = (maxE - minE);
        Double newHeight = (oldHeight / oldWidth) * width;
        xScale = width / oldWidth;
        yScale = newHeight / oldHeight;
    }
    
    // Accessors
    
    /**
     * Returns scenario filename.
     * @return String - Scenario file name.
     */
    public String getScenarioFileName()
    {
        return scenarioFileName;
    }
    
    /**
     * Returns regions read from file.
     * @return HashMap of regions read from file.
     */
    public HashMap<String, Region> getRegions()
    {
        return regions;
    }
    
    /**
     * Returns roads read from file.
     * @return HashMap of roads read from file.
     */
    public ArrayList<Road> getRoads()
    {
        return roads;
    }    

    /**
     * Returns X scale factor.
     * @return X scale factor.
     */    
    public Double getXScale()
    {
        return xScale;
    }

    /**
     * Returns Y scale factor.
     * @return Y scale factor.
     */      
    public Double getYScale()
    {
        return yScale;
    }

    /**
     * Returns X translation value.
     * @return X translation value.
     */      
    public Double getXTranslate()
    {
        return xTranslate;
    }

    /**
     * Returns Y translation value.
     * @return Y translation value.
     */    
    public Double getYTranslate()
    {
        return yTranslate;
    }
    
    /**
     * Returns minimum easting value.
     * @return minimum easting value.
     */    
    public Double getMinE()
    {
        return minE;
    }
    
    /**
     * Returns maximum easting value.
     * @return maximum easting value.
     */     
    public Double getMaxE()
    {
        return maxE;
    }

    /**
     * Returns minimum northing value.
     * @return minimum northing value.
     */     
    public Double getMinN()
    {
        return minN;
    }

    /**
     * Returns maximum northing value.
     * @return maximum northing value.
     */     
    public Double getMaxN()
    {
        return maxN;
    }

    /**
     * Returns height value.
     * @return height value.
     */     
    public Integer getHeight()
    {
        return height;
    }

    /**
     * Returns width value.
     * @return width value.
     */     
    public Integer getWidth()
    {
        return width;
    }

    /**
     * Returns whether to show labels.
     * @return Boolean flag.
     */  
    public boolean getLabelVisibility()
    {
        return labelVisibility;
    }

    /**
     * Set whether to show labels.
     * @return Boolean flag.
     */  
    public void setLabelVisibility( boolean labelVisibility )
    {
        this.labelVisibility = labelVisibility;
    }    
    
    /**
     * Enable rename road.
     * @return Boolean flag.
     */      
    public void enableRenameRoad( boolean renameRoad )
    {
        this.renameRoad = renameRoad;
    }
    
    /**
     * Check enable rename road flag.
     * @return Boolean flag.
     */     
    public boolean isRenameRoadEnabled()
    {
        return renameRoad;
    }
    
    /**
     * Enable shortest path flag.
     * @return Boolean flag.
     */     
    public void enableShowShortestPath( boolean showShortestPath )
    {
        this.showShortestPath = showShortestPath;
    }
    
    /**
     * Check show shortest path flag.
     * @return Boolean flag.
     */  
    public boolean isShowShortestPathEnabled()
    {
        return showShortestPath;
    } 
    
    /**
     * Enable change region colour.
     * @return Boolean flag.
     */     
    public void enableChangeRegionColor( boolean changeRegionColor )
    {
        this.changeRegionColor = changeRegionColor;        
    }
    
    /**
     * Check change region colour flag.
     * @return Boolean flag.
     */     
    public boolean isChangeRegionColorEnabled()
    {
        return changeRegionColor;
    }    
    
    /**
     * Checks whether a region has been selected.
     * @param p1 - point to check.
     * @return Region - the region that point is within.
     * Could add the functionality from the select road
     */     
    public Region selectRegion( Point2D p1, AffineTransform tr )
        throws NoninvertibleTransformException
    {
        Point2D p2 = tr.inverseTransform( p1, null );
        for( Region r : getRegions().values() )
        {
            if( r.getPath().contains( p2 ) )
            {
          		return r;
            }
        }

        return null;
    }

    /**
     * Return the shortest path between two points.
     * @return Path2D - Shortest path between two points.
     */     
    public Path2D getShortestPath( Point2D p1, Point2D p2, AffineTransform tr )
        throws NoninvertibleTransformException
    {
        Point2D p3 = tr.inverseTransform( p1, null );
        Point2D p4 = tr.inverseTransform( p2, null );
        return roadData.getShortestPath( p3, p4, 1000, "meters");
    }
    
    /**
     * Select a road from nearest point.
     * @return Road - Selected road.
     */     
    public Road selectRoad( Point2D p1, AffineTransform tr )
        throws NoninvertibleTransformException
    {
        Point2D p2 = tr.inverseTransform( p1, null );
        Road selectedRoad = roadData.findNearestRoad( p2, 1000 );
        return selectedRoad;
    }

    /**
     * Enable road data with original filename.
     */     
    public void saveRoadData()
        throws IOException
    {
        roadData.writeFile();
    }

    /**
     * Save road data with new filename.
     * @throws IOException - If the file was not saved correctly.
     */     
    public void saveRoadData( String filename )
        throws IOException    
    {
        roadData.writeFile( filename );
    }

    /**
     * Read a file containing scenario.
     * @param filename - Path and filename of file to read.
     * @throws IOException - If file cannot be read correctly.
     */      
    public void readScenarioFile( String filename )
        throws IOException
    {
        // Clear any existing data
        regions.clear();
        roads.clear();
        
        //String[] tokens = filename.split( "\\\\{1}" ); // Back slashes for Windows
        String[] tokens = filename.split( "\\/{1}" );  // Forward slashes for Mac
        
        String path = "";
        for ( int i = 0; i < tokens.length - 1; i++ )
        {
        	//path += tokens[i] + "\\";   // For Windows
        	path += tokens[i] + "//";   // For Mac
        }
        
        // Set the scenario filename loaded (to update status bar label text)
        this.scenarioFileName = filename;

        // Read the scenario file
        LineNumberReader input =
            new LineNumberReader(
                new FileReader( filename ) );

        // Read the files defined in scenario file
        String line = input.readLine();
        while( !line.equals( "finalend" ) )
        {
            if ( line.toLowerCase().contains( "road" ) )
            {
                roadData.setDataFileName( path + line );
                roadData.readFile();
                roads = roadData.getRoads();
            }
            else if (
                line.toLowerCase().contains( "victoria" ) ||
                line.toLowerCase().contains( "suburb" ) )
            {
                readFile( path + line );
            }
            else if ( line.toLowerCase().contains( "rail" ) )
            {
            
            }
                
            line = input.readLine();
        }

        fileIsLoaded = true;
    }
    
    /**
     * Read a file containing northing and easting values in Australia Map Grid format.
     * @param filename - Path and filename of file to read.
     * @throws IOException - If file cannot be read correctly.
     */     
    public void readFile( String filename )
        throws IOException
    {
    	//Setting flags to false
    	roadDataChanged = false;
    	regionDataChanged = false;
    	railDataChanged = false;

		// Create list of nodes
        ArrayList<Point2D> points = new ArrayList<Point2D>();

        
        LineNumberReader input =
            new LineNumberReader(
                new FileReader( filename ) );
        
        String line = input.readLine();
        String region = "";
        int count = 1;
        
        while( !line.equals( "finalend" ) )
        {
            maxE = 0.0;
            maxN = 0.0;
            minE = 0.0;
            minN = 0.0;
            
            //Road road = null;
            String[] tokens = null;
            
            while( !line.equals( "end" ) )
            {
                tokens = line.split( "\\|{1}" );
                region = tokens[0];
                easting = Double.parseDouble( tokens[1] );
                northing = Double.parseDouble( tokens[2] );
            
                //if( tokens.length == 7 && road == null )
                //    road = new Road( region );
            
                maxE = Math.max( easting, maxE );
                maxN = Math.max( northing, maxN );
                minE = Math.min( easting, maxE );
                minN = Math.min( northing, maxN );
                
                Point2D p1 = new Point2D.Double( easting, northing );
                points.add( p1 );
                
                line = input.readLine();
            }
            
            // Create new region for point data
            if ( tokens.length < 7 )
            {
            
            	Region r = new Region( region );
            
                // Set minimum and maximums for region
                r.setMaxE( maxE );
                r.setMaxN( maxN );
                r.setMinE( minE );
                r.setMinN( minN );
                
                // Give region unique name if rail or line
                if ( region.equals("Rail") || region.equals("Line") )
                {
                    region += count;
                    count++;
                }
                
                // Read latitude longitude data and create ArrayList of points
                r.createPath( points );
                
               
                // Add to regions HashMap
                regions.put( region, r );
                
                // Delete the ArrayList
                points.clear();
            }
            
            // Read next line
            line = input.readLine();
        }
        
        // Reset minimum and maximums across all regions
        for ( Region r : regions.values() )
        {
            maxE = Math.max( r.getMaxE(), maxE );
            maxN = Math.max( r.getMaxN(), maxN );
            minE = Math.min( r.getMinE(), minE );
            minN = Math.min( r.getMinN(), minN );        
        }

        // Reset scale
        resetScale();
    }
    
    /**
     * Sets the transparency value of suburb labels.
     * @param t - The transparency value
     */    
    public void setTransparency( int t )
    {
    	if( t <= 50 && t >= 0 )
    		this.transparencyFactor = t; 
    }

    /**
     * Returns label transparency factor.
     * @return int - Transparency factor for labels.
     */    
    public int getTransparencyFactor()
    {
    	return transparencyFactor;
    }

    /**
     * Sets the zoom rectangle used for MapPanel.
     * @param rectangle - The zoom rectange
     */    
    public void setRectangle( Path2D rectangle )
    {
		this.rectangle = rectangle;
	}

    /**
     * Gets the map panel zoom rectangle
     * @return Path2D - Zoom rectangle
     */
	public Path2D getRectangle()
    {
		return rectangle;
	}

    /**
     * Sets the flag to indicate road data has changed
     * @param roadDataChanged - flag to indicate road data has changes
     */ 
	public void setRoadDataChanged( boolean roadDataChanged )
    {
		this.roadDataChanged = roadDataChanged;
	}

    /**
     * Check the flag to indicate road data has changed
     * @return boolean - flag to indicate road data has changes
     */ 
	public boolean isRoadDataChanged()
    {
		return roadDataChanged;
	}

    /**
     * Set the flag to indicate region data has changed
     * @param regionDataChanged - flag to indicate region data has changes
     */    
	public void setRegionDataChanged( boolean regionDataChanged )
    {
		this.regionDataChanged = regionDataChanged;
	}

    /**
     * Check the flag to indicate region data has changed
     * @return boolean - flag to indicate region data has changes
     */     
	public boolean isRegionDataChanged()
    {
		return regionDataChanged;
	}

    /**
     * Set the flag to indicate rail data has changed
     * @param railDataChanged - flag to indicate rail data has changes
     */    
	public void setRailDataChanged( boolean railDataChanged )
    {
		this.railDataChanged = railDataChanged;
	}

    /**
     * Check the flag to indicate rail data has changed
     * @return boolean - flag to indicate rail data has changes
     */     
	public boolean isRailDataChanged()
    {
		return railDataChanged;
	}
}
